如何设计一个支持千万用户的照片分享系统?
点击关注公众号,AI,编程干货及时送达
让我们设计一个像Instagram一样的照片分享服务,用户可以上传照片与其他用户分享。
类似的服务:Flickr,Picasa 难度级别:中等
1. 什么是Instagram?
Instagram是一种社交网络服务,用户可以上传并分享他们的照片和视频给其他用户。Instagram用户可以选择公开或私下分享信息。任何公开分享的内容都可以被其他用户看到,而私下分享的内容只能被指定的一组人访问。Instagram还允许用户通过许多其他社交网络平台分享,如Facebook,Twitter,Flickr和Tumblr。
为了这个练习,我们计划设计一个更简单的版本的Instagram,用户可以分享照片并且也可以关注其他用户。每个用户的'新闻动态'将包括用户关注的所有人的热门照片。
2. 系统的要求和目标
在设计Instagram时,我们将重点关注以下一组要求:
功能要求
1. 用户应能够上传/下载/查看照片。
2. 用户可以根据照片/视频标题进行搜索。
3. 用户可以关注其他用户。
4. 系统应能够生成并显示用户的新闻动态,包括用户关注的所有人的热门照片。
非功能性要求
1. 我们的服务需要高度可用。
2. 系统的可接受延迟是新闻动态生成的200ms。
3. 为了可用性,一致性可以受到影响,如果用户没有看到一张照片一段时间;那应该没问题。
4. 系统应该高度可靠;任何上传的照片或视频都不应该丢失。
不在范围内:给照片添加标签,根据标签搜索照片,评论照片,给照片标记用户,关注谁等。
3. 一些设计考虑
系统将是读取重的,所以我们将专注于构建一个可以快速检索照片的系统。
1. 实际上,用户可以上传他们喜欢的尽可能多的照片。在设计这个系统时,有效的存储管理应该是一个关键因素。
2. 查看照片时,期望低延迟。
3. 数据应该100%可靠。如果用户上传了一张照片,系统将保证它永远不会丢失。
4. 容量估计和约束
• 假设我们总共有500M用户,每天有1M活跃用户。
• 每天新上传2M张照片,每秒23张新照片。
• 平均照片文件大小 => 200KB
• 一天内存储照片所需的总空间
2M * 200KB => 400 GB
• 10年所需的总空间:
400GB * 365 (一年的天数) * 10 (年) ~= 1425TB
5. 高级系统设计
在高层次上,我们需要支持两种场景,一种是上传照片,另一种是查看/搜索照片。我们的服务将需要一些对象存储服务器来存储照片,以及一些数据库服务器来存储关于照片的元数据信息。
6. 数据库架构
💡 在面试的早期阶段定义数据库架构有助于理解各个组件之间的数据流,后期则有助于数据分区。
我们需要存储关于用户、他们上传的照片以及他们关注的人的数据。照片表将存储与照片相关的所有数据;我们需要在(PhotoID,CreationDate)上建立索引,因为我们需要先获取最新的照片。
存储上述架构的直接方法是使用像MySQL这样的RDBMS,因为我们需要连接。但是关系数据库带来了挑战,特别是当我们需要扩展它们的时候。有关详细信息,请参考SQL与NoSQL的比较。
我们可以在分布式文件存储如HDFS或S3中存储照片。
我们可以将上述架构存储在分布式键值存储中,以享受NoSQL提供的好处。所有与照片相关的元数据可以放入一个表中,其中'键'将是'PhotoID','值'将是一个包含PhotoLocation, UserLocation, CreationTimestamp等的对象。
我们需要存储用户和照片之间的关系,以知道谁拥有哪张照片。我们还需要存储用户关注的人的列表。对于这两个表,我们可以使用像Cassandra这样的宽列数据存储。对于‘UserPhoto’表,‘键’将是‘UserID’,‘值’将是用户拥有的‘PhotoIDs’列表,存储在不同的列中。我们将对‘UserFollow’表有类似的方案。
Cassandra或一般的键值存储总是维护一定数量的副本以提供可靠性。此外,在这样的数据存储中,删除不会立即应用,数据会保留一定的天数(支持恢复删除)再从系统中永久删除。
7. 数据大小估计
让我们估计每个表中将有多少数据,以及我们在10年内需要多少总存储空间。
用户:假设每个“int”和“dateTime”是四字节,用户表中的每一行将是68字节:
UserID (4 bytes) + Name (20 bytes) + Email (32 bytes) + DateOfBirth (4 bytes) +
CreationDate (4 bytes) + LastLogin (4 bytes) = 68 bytes
如果我们有5亿用户,我们将需要32GB的总存储空间。
500 million * 68 ~= 32GB
照片:照片表中的每一行将是284字节:
PhotoID (4 bytes) + UserID (4 bytes) + PhotoPath (256 bytes) + PhotoLatitude (4
bytes) + PhotLongitude(4 bytes) + UserLatitude (4 bytes) + UserLongitude (4 bytes)
+ CreationDate (4 bytes) = 284 bytes
如果每天有2M新照片被上传,我们将需要0.5GB的存储空间存放一天的数据:
2M * 284 bytes ~= 0.5GB per day
10年后我们将需要1.88TB的存储空间。
UserFollow:UserFollow表中的每一行将占用8字节。如果我们有5亿用户,平均每个用户关注500个用户。我们将需要1.82TB的存储空间用于UserFollow表:
500 million users * 500 followers * 8 bytes ~= 1.82TB
10年内所有表需要的总空间将是3.7TB:
32GB + 1.88TB + 1.82TB ~= 3.7TB
8. 组件设计
照片上传(或写入)可能会比较慢,因为它们需要写入硬盘,而读取则会更快,尤其是如果它们是从缓存中获取的。
上传用户可能会占用所有可用的连接,因为上传是一个慢的过程。这意味着如果系统忙于所有的写入请求,'读取'不能被服务。我们在设计系统前应该记住,web服务器有一个连接限制。如果我们假设一个web服务器在任何时候最多可以有500个连接,那么它不能有超过500个并发上传或读取。为了处理这个瓶颈,我们可以将读取和写入分成独立的服务。我们将有专门的服务器进行读取和不同的服务器进行写入,以确保上传不会占用系统。
将照片的读取和写入请求分开也将使我们能够独立地扩展和优化这些操作。
9. 可靠性和冗余
丢失文件对我们的服务来说是不可接受的。因此,我们将存储每个文件的多个副本,以便如果一个存储服务器死掉,我们可以从存在于另一个存储服务器上的另一个副本中检索照片。
这个原则同样适用于系统的其他组件。如果我们想要系统具有高可用性,我们需要在系统中运行多个服务的副本,以便如果一些服务挂掉,系统仍然可以运行并提供服务。冗余消除了系统中的单点故障。
如果只需要在任何时点运行一个服务的实例,我们可以运行一个冗余的次要服务副本,它不提供任何流量服务,但是当主服务出现问题时,它可以接管控制权。
在系统中创建冗余可以消除单点故障,并在危机中提供备份或备用功能。例如,如果有两个相同服务的实例在生产中运行,而其中一个失败或降级,系统可以切换到健康的副本。故障转移可以自动发生,也可以需要人工干预。
10. 数据分片
让我们讨论一下元数据分片的不同方案:
a. 基于用户ID(UserID)的分区 假设我们基于'用户ID(UserID)'进行分片,这样我们可以将一个用户的所有照片保存在同一个分片上。如果一个数据库分片为1TB,我们需要四个分片来存储3.7TB的数据。为了更好的性能和可扩展性,我们假设有10个分片。
所以我们通过 UserID % 10 来找到分片号,然后在那里存储数据。为了在我们的系统中唯一地识别任何照片,我们可以将分片号与每个照片ID(PhotoID)一起附加。
我们如何生成照片ID(PhotoIDs)? 每个数据库分片都可以有自己的自动增长序列来生成照片ID,因为我们会将分片ID(ShardID)与每个照片ID一起附加,这将使其在我们的系统中保持唯一。
这个分区方案有什么问题?
1. 我们如何处理热门用户?很多人关注这样的热门用户,很多其他人看他们上传的任何照片。
2. 有些用户的照片比其他人多,从而导致存储分布不均。
3. 如果我们不能将一个用户的所有照片都存储在一个分片上会怎样?如果我们将一个用户的照片分布到多个分片上,会不会造成更高的延迟?
4. 将一个用户的所有照片存储在一个分片上可能会导致一些问题,如如果该分片不可用,该用户的所有数据都不可用,或者如果它正在提供高负载服务,延迟会更高等。
b. 基于照片ID(PhotoID)的分区 如果我们能首先生成唯一的照片ID,然后通过"照片ID % 10"找到分片号,上述问题就能得到解决。在这种情况下,我们不需要将分片ID附加到照片ID,因为照片ID本身在整个系统中都是唯一的。
我们如何生成照片ID?在这里,我们不能在每个分片中都定义一个自动递增的序列来定义照片ID,因为我们需要首先知道照片ID,才能找到存储它的分片。一个解决方案是我们专门设立一个单独的数据库实例来生成自动递增的ID。如果我们的照片ID可以放入64位,我们可以定义一个只包含64位ID字段的表。所以每当我们想在系统中添加一张照片,我们可以在这个表中插入一个新的行,并把那个ID作为新照片的照片ID。
这个生成键的数据库不就成了单点故障吗? 是的,的确是。解决这个问题的一个方法是定义两个这样的数据库,一个生成偶数ID,另一个生成奇数ID。对于MySQL,以下脚本可以定义这样的序列:
我们可以在这两个数据库前面放一个负载均衡器,进行轮询,并处理停机时间。这两个服务器可能会不同步,一个产生的键比另一个多,但这不会在我们的系统中引起任何问题。我们可以通过为用户、照片评论或系统中存在的其他对象定义单独的ID表来扩展这个设计。
或者,我们可以实现一个类似于我们在设计类似TinyURL的URL缩短服务中讨论过的'键'生成方案。
我们如何规划我们系统的未来增长?我们可以有大量的逻辑分区来容纳未来的数据增长,这样在开始时,多个逻辑分区可以驻留在一个物理数据库服务器上。由于每个数据库服务器上可以有多个数据库实例,我们可以在任何服务器上为每个逻辑分区有单独的数据库。所以每当我们觉得某个数据库服务器的数据量很大,我们可以将一些逻辑分区从它迁移到另一个服务器。我们可以维护一个配置文件(或一个单独的数据库),它可以将我们的逻辑分区映射到数据库服务器;这将使我们可以很容易地移动分区。每当我们想要移动一个分区,我们只需要更新配置文件来宣布这个变化。
11. 排名和新闻推送生成
要为任何特定用户创建新闻推送,我们需要获取用户关注的人的最新、最受欢迎和相关的照片。
为了简化,我们假设我们需要为用户的新闻推送获取前100张照片。我们的应用服务器首先会获取用户关注的人的列表,然后从每个用户那里获取最新的100张照片的元数据信息。最后一步,服务器将所有这些照片提交给我们的排名算法,该算法将根据最新性、喜好度等因素确定前100张照片,并将它们返回给用户。这种方法可能的问题是延迟较高,因为我们必须查询多个表,并对结果进行排序/合并/排名。为了提高效率,我们可以预生成新闻推送并将其存储在单独的表中。
预生成新闻推送:我们可以有专门的服务器持续生成用户的新闻推送,并将它们存储在‘UserNewsFeed(用户新闻推送)’表中。所以,每当任何用户需要最新的照片作为他们的新闻推送时,我们只需查询这个表,然后将结果返回给用户。
每当这些服务器需要生成用户的新闻推送时,它们首先会查询UserNewsFeed表,找出上次为该用户生成新闻推送的时间。然后,将从那个时间点开始生成新的新闻推送数据(按照上述步骤进行)。
发送新闻推送内容给用户有哪些不同的方法?
1. 拉取:客户端可以定期从服务器拉取新闻推送内容,或者在需要的时候手动拉取。这种方法可能的问题是 a) 新的数据可能在客户端发出拉取请求之前不会展示给用户 b) 如果没有新的数据,大部分时间拉取请求都会得到空响应。
2. 推送:服务器可以在新数据可用时立即将其推送给用户。为了有效地管理这个过程,用户需要与服务器维持长轮询请求,以接收更新。这种方法可能的问题是,一个关注了很多人的用户,或者一个有数百万粉丝的名人用户;在这种情况下,服务器必须频繁地推送更新。
3. 混合:我们可以采取混合方法。我们可以将所有关注人数多的用户转移到基于拉取的模型,只向那些关注几百(或几千)人的用户推送数据。另一种方法是,服务器推送更新给所有用户的频率不超过某个值,让关注人数多/更新多的用户定期拉取数据。
关于新闻推送生成的详细讨论,请参考'设计Facebook的新闻推送(Designing Facebook’s Newsfeed)'。
12. 分片数据的新闻推送创建
为任何特定用户创建新闻推送的最重要的需求是获取用户关注的所有人的最新照片。为此,我们需要有一种机制来根据照片的创建时间进行排序。为了有效地做到这一点,我们可以将照片的创建时间作为PhotoID的一部分。由于我们在PhotoID上有主索引,因此找到最新的PhotoID将非常快。
我们可以使用纪元时间来实现这一点。假设我们的PhotoID有两部分;第一部分将表示纪元时间,第二部分将是自增序列。因此,要生成一个新的PhotoID,我们可以取当前的纪元时间,然后附加一个来自我们的键生成数据库的自增ID。我们可以从这个PhotoID中找出分片的编号( PhotoID % 10)并在那里存储照片。
我们的PhotoID的大小可能是多少呢?假设我们的纪元时间从今天开始,我们需要多少位来存储接下来50年的秒数?
每天86400秒 * 365(一年的天数)* 50(年)=> 16亿秒
我们需要31位来存储这个数字。因为平均来说,我们预计每秒会有23张新照片;所以我们可以分配9位来存储自增序列。所以我们每秒可以存储(2^9 => 512)张新照片。我们可以每秒重置我们的自增序列。
我们将在'数据分片(Data Sharding)'部分讨论这种技术的更多细节,参见'设计Twitter(Designing Twitter)'。
13. 缓存和负载均衡
我们的服务将需要一个大规模的照片传输系统来服务全球分布的用户。我们的服务应该使用大量地理分布的照片缓存服务器,使用CDNs(详情参见'缓存(Caching)')将内容推送到用户附近。
我们可以为元数据服务器引入一个缓存,以缓存热门的数据库行。我们可以使用Memcache来缓存数据,应用服务器在访问数据库之前可以快速检查缓存是否有需要的行。最近最少使用(LRU)可以是我们系统的一个合理的缓存驱逐策略。在这个策略下,我们首先丢弃最近最少查看的行。
我们如何构建更智能的缓存?如果我们遵循80-20规则,即20%的每日照片阅读量产生80%的流量,这意味着某些照片非常受欢迎,大多数人都在阅读它们。这告诉我们,我们可以试着缓存20%的每日照片和元数据的阅读量。
推荐阅读
你好,我是拾叁,7年开发老司机、互联网两年外企5年。怼得过阿三老美,也被PR comments搞崩溃过。这些年我打过工,创过业,接过私活,也混过upwork。赚过钱也亏过钱。一路过来,给我最深的感受就是不管学什么,一定要不断学习。只要你能坚持下来,就很容易实现弯道超车!所以,不要问我现在干什么是否来得及。如果你还没什么方向,可以先关注我,这里会经常分享一些前沿资讯和编程知识,帮你积累弯道超车的资本。